<?php
/**
 * pdo扩展访问操作数据库
 * Created by PhpStorm.
 * User: Inhere
 * Date: 14-6-2
 * Time: 下午3:54
 */
namespace ulue\core\db\base\pdo;

use PDO,
    Debug;
use ulue\core\db\base\AbstractDbDriver;
use ulue\libs\helpers\ArrayHelper;
use ulue\core\db\TheDatabaseException;

class PdoDriver extends AbstractDbDriver
{
    protected $name = 'pdo';

    /**
     * 预处理(PDOStatement)对象
     * @var \PDOStatement
     */
    protected $cursor;

    /**
     * Property driverOptions.
     * @var PDO
     */
    protected $connect;

    /**
     * Property driverOptions.
     * @var mixed
     */
    protected $driverOptions;

    private $isBeginTrans       = false; //是否开启事务

    public $caseMode;          // @see getCaseMode()

    public $fetchMode;          // @see getFetchMode()


    public function __construct(\PDO $connect = null, $options = array())
    {
        // Finalize initialisation
        parent::__construct($connect, $options);

        $this->driverOptions = $this->getOption('driverOptions');
    }

    /**
     * 数据库连接、选择 返回数据库连接对象
     * @throws TheDatabaseException
     * @return \PDO $this->connect
     */
    public function connect()
    {
        if ($this->connect) {
            return $this;
        }

        try {
            $dbInstance = new PDO(
                $this->options['dsn'],
                $this->options['username'],
                $this->options['password'],
                $this->options['driverOptions']
                );

            $dbInstance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        } catch (TheDatabaseException $e) {
            throw new TheDatabaseException( 'Database connect failed!'.$e->getMessage(),'db' );
        }

        $this->connect = $dbInstance;

        # 字符集
        // $this->setCharset();
        $this->getDatabase();

        return $this;
    }

    public function select($database)
    {
        $this->database = $database;

        $this->getDatabase($database)->select();

        return $this;
    }

    /**
     * 执行语句 返回 受影响的行数
     * 适用于 insert update delete 等...
     * @param string | object $sql
     * 1 可以是直接的sql语句
     * 2 也可以是 $this->prepare()返回的 PDOStatement 对象, 此方法不支持 绑定参数
     * (对于需要发出多次的语句，可用  $this->prepare()  来准备一个 PDOStatement 对象并用  $this->execute()  发出语句。 )
     * @return int
     */
    public function exec( $sql = '')
    {
        if ($sql) {
            $this->setSql($sql);
        }

        $sql = $this->getRealSql();

        if ($this->params) {
            $sql = str_replace(array_keys($this->params), $this->params, $sql);
        }

        // >> 将SQL添加到调试DEBUG,记录开始时间
        $this->debug($sql,microtime(true));

            $this->affectedRows = $this->connect->exec($sql);

        // >> 记录开始结束时间
        $this->debug('',microtime(true));

        return $this->affectedRows;
    }

    /**
     * 发送并执行查询语句 返回 cursor, Todo 不会绑定参数
     * @param string $sql [description]
     * @return object $PdoStatement returning a result set as a PDOStatement object , or false on failure
     */
    public function query($sql='')
    {
        if ($sql) {
            $this->setSql($sql);
        }

        $sql = $this->getRealSql();

        if (!empty($this->params)) {
            $sql = str_replace(array_keys($this->params), $this->params, $sql);
        }

        // >> 将SQL添加到调试DEBUG,记录开始时间
        $this->debug($sql);

            $PdoStatement      = $this->connect->query($sql);

        // >> 记录开始结束时间
        $this->debug();

        return ($this->cursor = $PdoStatement);
   }

    /**
     * 预处理
     * @throws TheDatabaseException
     * @return \PDOStatement $cursor
     */
    public function prepare()
    {
        if ($this->cursor) {
            return $this;
        }

        $sql          = $this->getRealSql();

        try {
            Debug::trace("i 预准备SQL语句", 'SQL ：'.$sql );
            $this->cursor = $this->connect->prepare($sql);

        } catch (\PDOException $e) {
            throw new TheDatabaseException(' PDO预准备 prepare() 执行失败! SQL: '.$sql.'；INFO: '.$e->getMessage());
        }

        return $this;
    }

    public function cancel()
    {
        $this->cursor = null;
    }

/*
    1.通过绑定 PHP 变量执行一条预处理语句 -- 最灵活，且可以验证过滤数据
 $calories  =  150 ;
 $colour  =  'red' ;
 $sth  =  $dbh -> prepare ( 'SELECT name, colour, calories  FROM fruit  WHERE calories < :calories AND colour = :colour' );
 $sth -> bindParam ( ':calories' ,  $calories ,  PDO :: PARAM_INT );
 $sth -> bindParam ( ':colour' ,  $colour ,  PDO :: PARAM_STR ,  12 );
 $sth -> execute ();
    2.通过传递一个含有插入值的数组执行一条预处理语句
 $calories  =  150 ;
 $colour  =  'red' ;
 $sth  =  $dbh -> prepare ( 'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour' );
 $sth -> execute (array( ':calories'  =>  $calories ,  ':colour'  =>  $colour ));
    3.通过传递一个插入值的数组执行一条预处理语句 ( ? 问号占位符 ) -- 最快捷
 $calories  =  150 ;
 $colour  =  'red' ;
 $sth  =  $dbh -> prepare ( 'SELECT name, colour, calories  FROM fruit  WHERE calories < ? AND colour = ?' );
 $sth -> execute (array( $calories ,  $colour ));

*/
    /**
     * 执行已准备[prepare()]好的sql语句
     * @param  array|string $params [description]
     * @throws TheDatabaseException
     * @return bool
     */
    public function execute(array $params=[] )
    {
        //
        $params   = array_merge( $this->params, $params);
        $paramStr = '';

        Debug::trace("i 添加绑定参数，开始执行SQL语句。".Debug::countTime(),[
            '@property $sql' => $this->sql,
            '@param $params' => $params]
            );

        if ( $params ) {
            $paramStr = ArrayHelper::toString($params);
            $paramStr = !$paramStr ? '' : ' ; [ bound with '.$paramStr.' ]';
        }

        // 预准备语句(会自动判断是否已准备)
        $this->prepare();

        // >> 将SQL添加到调试DEBUG,记录执行开始时间
        $this->debug( $this->sql.$paramStr );

        try {
            $this->cursor->execute($params);

        } catch (\Exception $e) {
            throw new TheDatabaseException('SQL语句执行[execute()]失败! <br> '.$this->getDebugSql().' ; <br>INFO: '.$e->getMessage());
        }

        // >> 记录执行结束时间
        $this->debug();

        $this->affectedRows = $this->cursor->rowCount();

        // 查询参数初始化
        $this->queryReset();

        unset($sql,$params);

        return $this->affectedRows;
    }

    /**
     * 绑定一个参数
     * @param $param
     * @param $var
     * @param int $dataType
     * @param  integer $length [description]
     * @param  string $driver_options [description]
     * @return $this
     */
    public function bindParam( $param, $var, $dataType = PDO::PARAM_STR, $length=12, $driver_options='' )
    {
        if ($this->cursor) {
            $this->cursor->bindParam($param, $var, $dataType, $length, $driver_options);
        }

        return $this;
    }

    //绑定一个值,静态值，不能是变量
    public function bindValue( $parameter , $value, $dataType = PDO::PARAM_STR  )
    {
        if ($this->cursor) {
            $this->cursor->bindValue($parameter , $value, $dataType);
        }

        return $this;
    }

    //绑定多个值,静态值，不能是变量
    public function bindValues( $values)
    {
        if ($this->cursor) {
            foreach ($values as $value) {
                $this->cursor->bindValue($value['param'] , $value['value'], isset($value['type']) ? $value['type'] : PDO::PARAM_STR );
            }
        }

        return $this;
    }


    /**
     * @return mixed
     */
    public function fetchArray()
    {
        return $this->fetch(\PDO::FETCH_NUM);
    }

    /**
     * @return  mixed
     */
    public function fetchAssoc()
    {
        return $this->fetch(\PDO::FETCH_ASSOC);
    }

    /**
     * @param string $class
     * @param array $ctorArgs
     * @return mixed
     */
    public function fetchObject($class='\stdClass', array $ctorArgs= [])
    {
        /**
         * @var $stmt \PDOStatement
         */
        if ($stmt = $this->cursor) {
            $this->resultSet = $stmt->fetchObject($class, $ctorArgs);
        }

        return $this->resultSet;
    }

    /**
     * bool PDOStatement::setFetchMode  ( int $mode  ) // $mode 获取模式必须是 PDO::FETCH_* 系列常量中的一个。
     * bool PDOStatement::setFetchMode  ( int $PDO::FETCH_COLUMN  , int $colno[列号]  )
     * bool PDOStatement::setFetchMode  ( int $PDO::FETCH_CLASS  , string $classname[类名]  , array $ctorargs[构造函数参数]  )
     * bool PDOStatement::setFetchMode  ( int $PDO::FETCH_INTO  , object $object[对象]  )
     * fetch value action
     * @param string $type
     *  e.g. $db->setMode('assoc')->readOne();
     * @throws \DInvalidArgumentException
     * @return int
     */
    public function setMode($type='assoc')
    {
        $modes = [
            'assoc' => PDO::FETCH_ASSOC,
            'both'  => PDO::FETCH_BOTH,
            'bound' => PDO::FETCH_BOUND,
            'class' => PDO::FETCH_CLASS,
            'into'  => PDO::FETCH_INTO,
            'lazy'  => PDO::FETCH_LAZY,
            'num'   => PDO::FETCH_NUM,
            'obj'   => PDO::FETCH_OBJ
        ];

        $args = func_get_args();
        $type = array_shift($args);

        if ( !isset($modes[$type]) ) {
            $info = sprintf('数据获取模式(fetchMode)中不存在[%s]类型！可用类型有%s',$type, implode(',', array_keys($modes)));
            throw new \DInvalidArgumentException($info);
        }

        $type = $this->fetchMode = $modes[$type];
        $args = array_merge([$type], $args);

        call_user_func_array([$this->cursor, 'setFetchMode'], $args);

        return $this;
    }

    /**
     * 遍历结果集 从结果集中获取下一行
     * @param int $type
     * 控制下一行如何返回给调用者。此值必须是 PDO::FETCH_* 系列常量中的一个，
     * 缺省为 PDO::ATTR_DEFAULT_FETCH_MODE 的值 （默认为 PDO::FETCH_BOTH ）。
     * 1. PDO::FETCH_ASSOC -- 返回一个索引为结果集列名的数组
     * 2. PDO::FETCH_BOTH(默认) -- 返回一个索引为结果集列名和以0开始的列号的数组
     * 3. PDO::FETCH_BOUND -- 返回 true  ，并分配结果集中的列值给 PDOStatement::bindColumn()  方法绑定的 PHP 变量。
     * 4. PDO::FETCH_CLASS -- 返回一个请求类的新实例，映射结果集中的列名到类中对应的属性名。如果 type 包含 PDO::FETCH_CLASSTYPE（例如：PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE），则类名由第一列的值决定
     * 5. PDO::FETCH_INTO -- 更新一个被请求类已存在的实例，映射结果集中的列到类中命名的属性
     * 6. PDO::FETCH_LAZY -- 结合使用 PDO::FETCH_BOTH 和 PDO::FETCH_OBJ，创建供用来访问的对象变量名
     * 7. PDO::FETCH_NUM -- 返回一个索引为以0开始的结果集列号的数组
     * 8. PDO::FETCH_OBJ -- 返回一个属性名对应结果集列名的匿名对象
     * @param int $orientation
     * @param  integer $offset [description]
     * @return mixed
     */
    public function fetch( $type=PDO::FETCH_ASSOC, $orientation= PDO::FETCH_ORI_NEXT , $offset=0 )
    {
        /**
         * @var $stmt \PDOStatement
         */
        if ($stmt = $this->cursor) {
            $this->resultSet = $stmt->fetch($type, $orientation, $offset);
        }

        return $this->resultSet;
    }

    /**
     * 遍历结果集
     * @param int $type 同 $this->fetch() --> @param $type
     * @param null $argument
     * @param array|null $ctorArgs 当 type 参数为 PDO::FETCH_CLASS  时，自定义类的构造函数的参数。
     * @return mixed
     */
    public function fetchAll( $type=PDO::FETCH_ASSOC , $argument=null , array $ctorArgs = []  )
    {
        /**
         * @var $stmt \PDOStatement
         */
        if ($stmt = $this->cursor) {
            $this->resultSet = $stmt->fetchAll( $type);
            // $this->resultSet = $stmt->fetchAll( $type, $argument , $ctorArgs); //@todo 添加其他参数时发生错误
        }

        return $this->resultSet;
    }

    public function getCursor()
    {
        return $this->cursor;
    }

    public function setCursor(\PDOStatement $cursor)
    {
        $this->cursor = $cursor;

        return $this;
    }

    public function getDebugSql()
    {
        ob_start();
        $this->cursor->debugDumpParams();
        return ob_get_clean();
    }

    // 返回由 PDOStatement 对象代表的结果集中的列数。
    public function columnCount()
    {
        return $this->cursor ? $this->cursor->columnCount() : null;
    }

    // 返回由 PDOStatement 对象代表的结果集中的列数。
    public function fetchColumn($int=0)
    {
        return $this->cursor ? $this->cursor->fetchColumn($int) : null;
    }

    /**
     * @return string
     */
    public function getInsertId()
    {
        return $this->connect->lastInsertId();
    }

    /**
     * Method to free up the memory used for the result set.
     * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
     * @return  static
     */
    public function freeResult($cursor = null)
    {
        if ($this->cursor instanceof \PDOStatement) {
            $this->cursor->closeCursor();

            $this->cursor = null;
        }

        return $this;
    }

    /**
     * 开启事务处理
     * @class \PDO::beginTransaction()
     */
    public function beginTrans()
    {
        if (!$this->isBeginTrans) {
            $this->connect->beginTransaction();
            $this->isBeginTrans = true;
        }
    }


    /**
     *提交一个事务
     */
    public function commit()
    {
        if ($this->isBeginTrans) {
            $this->connect->commit();

            $this->isBeginTrans = false;
        }
    }


    /**
     *回滚事务
     */
    public function rollback()
    {
        if ($this->isBeginTrans) {
            $this->connect->rollback();

            $this->isBeginTrans = false;
        }
    }


     //操作错误
    /**
     * @return array
     */
    protected function errorInfo()
    {
        return $this->connect->errorInfo();
    }

    // 获得MYSQL版本信息
    public function getVersion()
    {
        return $this->connect->getAttribute(PDO::ATTR_SERVER_VERSION);
    }

    // 错误代码
    /**
     * @return mixed
     */
    protected function errorCode()
    {
        return $this->connect->errorCode();
    }

    /**
     * 防止外部克隆
     */
    private function __clone()
    {}

    /**
     * 关闭连接
     */
    public function close()
    {
        if (is_object($this->connect) ) {
            $this->freeResult();
            $this->connect = null;
        }
    }

}